﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.Security.Principal;
using System.Transactions;
using System.Threading;
using System.Data.Linq.SqlClient;
using EcclesCollege.Tools;

namespace DeviceSync
{
	[ServiceBehavior(InstanceContextMode=InstanceContextMode.PerCall, ConcurrencyMode=ConcurrencyMode.Multiple)]
	public class SyncService : ISyncService
	{
		// Thread-safe cache of fileid + owner username
		private static SafeDict<int, string> FileOwners = new SafeDict<int, string>(100, 30);

		private static string GetUsername()
		{
			WindowsIdentity callerWindowsIdentity = ServiceSecurityContext.Current.WindowsIdentity;
			string s = callerWindowsIdentity.Name.ToLower();
			if (s.Contains('\\'))
				s = s.Substring(s.LastIndexOf('\\') + 1).Trim();

			if (string.IsNullOrEmpty(s)) throw new Exception("Cannot find username");

			return s;
		}

		/// <summary>
		/// Returns base64 encoded SHA1 hash of the supplied data
		/// </summary>
		public static string HashData(byte[] data, int len)
		{
			if (data == null || data.Length == 0) return "";
			System.Security.Cryptography.SHA1Managed sha = new System.Security.Cryptography.SHA1Managed();
			return System.Convert.ToBase64String(sha.ComputeHash(data, 0, len));
		}

		/// <summary>
		/// Web Method - saves file header record, no file content is passed here
		/// </summary>
		public int SendFileRec(string filename, DateTime lastchange)
		{
			string username = GetUsername();
			using (var con = new SyncDataClassesDataContext())
			{

				CommsProvider.SyncedFiles newfile = new CommsProvider.SyncedFiles();
				newfile.filename = filename;
				newfile.username = username;
				newfile.lastchange = lastchange;
				newfile.closed = false;

				con.SyncedFiles.InsertOnSubmit(newfile);
				con.SubmitChanges();

				return newfile.fid;
			}
		}

		/// <summary>
		/// Internal - saves a file fragment using existing db context
		/// </summary>
		private string SendFragmentInternal(SyncDataClassesDataContext con, string username, int fileid, int fragment, byte[] data, int datasize, string hash)
		{
			if (GetOwner(fileid, con) != username)
				throw new Exception("Invalid fileid - not same owner");

			CommsProvider.FileFragments frag = new CommsProvider.FileFragments();
			frag.forfile = fileid;
			frag.fragmentid = fragment;
			if (datasize < data.Length)
			{
				byte[] temp = new byte[datasize];
				Array.Copy(data, temp, datasize);
				frag.data = temp;
				data = temp;
			}
			else
				frag.data = data;

			string serverhash = HashData(data, datasize);

			if (hash != null)
			{
				// check against supplied hash
				if (hash != serverhash)
					throw new Exception("Hash does not match for fragment");
			}

			con.FileFragments.InsertOnSubmit(frag);

			return serverhash;
		}

		/// <summary>
		/// Web Method - saves a file fragment to db
		/// </summary>
		public string SendFragment(int fileid, int fragment, byte[] data, int datasize)
		{
			string username = GetUsername();
			using (var con = new SyncDataClassesDataContext())
			{
				string s = SendFragmentInternal(con, username, fileid, fragment, data, datasize, null);
				con.SubmitChanges();

				return s;
			}
		}

		/// <summary>
		/// Web Method - saves a file fragment, and optionally 'closes' the file
		/// </summary>
		public string SendFragmentClosure(int fileid, int fragment, byte[] data, int datasize, bool closeafter)
		{
			string username = GetUsername();
			using (var con = new SyncDataClassesDataContext())
			{
				string s = SendFragmentInternal(con, username, fileid, fragment, data, datasize, null);

				if (closeafter)
					CloseFileInternal(con, username, fileid);

				con.SubmitChanges();

				return s;
			}
		}

		/// <summary>
		/// Web Method - saves a file fragment, and optionally 'closes' the file
		/// </summary>
		public void SendFragmentHashClosure(int fileid, int fragment, byte[] data, int datasize, string hashcheck, bool closeafter)
		{
			string username = GetUsername();
			using (var con = new SyncDataClassesDataContext())
			{
				string s = SendFragmentInternal(con, username, fileid, fragment, data, datasize, hashcheck);

				if (closeafter)
					CloseFileInternal(con, username, fileid);

				con.SubmitChanges();
			}
		}

		/// <summary>
		/// Internal. Marks file as closed
		/// </summary>
		private void CloseFileInternal(SyncDataClassesDataContext con, string username, int fileid)
		{
			var rec = con.SyncedFiles
				.Where(f => f.fid == fileid && f.username == username)
				.Single();

			rec.closed = true;
		}

		/// <summary>
		/// Web Method - mark fileid as closed (fully uploaded)
		/// </summary>
		public void CloseFile(int fileid)
		{
			string username = GetUsername();
			using (var con = new SyncDataClassesDataContext())
			{
				CloseFileInternal(con, username, fileid);
				con.SubmitChanges();
			}
		}

		/// <summary>
		/// Web Method - required client version
		/// </summary>
		public Version ClientVersion()
		{
			return new Version(1, 0, 2, 0);
		}

		/// <summary>
		/// Web Method - gets server time
		/// </summary>
		public DateTime ServerTime()
		{
			return DateTime.Now;
		}

		/// <summary>
		/// Web Method - gets server time and required client version
		/// </summary>
		public void StartupChecks(out DateTime servertime, out Version requiredversion)
		{
			servertime = ServerTime();
			requiredversion = ClientVersion();
		}

		/// <summary>
		/// Gets username owner of fileid. Uses thread-safe cache to reduce db access (stateless service)
		/// </summary>
		private string GetOwner(int fileid, SyncDataClassesDataContext con)
		{
			string owner;
			if (FileOwners.TryGetValue(fileid, out owner))
				return owner;

			owner = con.SyncedFiles
				.Where(f => f.fid == fileid)
				.Select(f => f.username)
				.Single();

			FileOwners.SetValue(fileid, owner);

			return owner;
		}
	}

}
